import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Random;

import javax.imageio.ImageIO;

/**
 * Takes an image, determines what features are important, and generates
 * a simpler representation for the neural network
 * 
 * @author Stephen Mues (sgmr29@gmail.com)
 *
 */
public class ClusteringMethod implements FeatureExtractionMethod {
	//The length added to the output to represent each cluster
	public static int DOUBLES_PER_CLUSTER = 6;
	//Do not iterate more than this many times
	public static final int MAX_ITERATIONS = 15;
	//Stop iterating when less than this many pixels change clusters
	public static final int MIN_PIXELS_CHANGED = 25;
	//Set to true if you want to see what the cluster assignments look like
	static final boolean VISUALIZE_ASSIGNMENTS = true;
	//Set to true if you want to see what information will look like to the
	//neural network
	static final boolean VISUALIZE_CLUSTERS = true;
	//Set to true to see console output each time an image is processed
	static final boolean LOGGING = true;
	
	/**
	 * This function will likely use one of the many techniques presented by wikipedia:
	 * http://en.wikipedia.org/wiki/Segmentation_%28image_processing%29
	 * 
	 * @param	image	The raw image to be processed
	 * @param	outputLength	The length of the returned list
	 * @return	A list of Doubles between 0 and 1 that summarize the image
	 */
	public ArrayList<Double> process(BufferedImage image, int outputLength)
	{
		int clusterCount = outputLength / DOUBLES_PER_CLUSTER; //Intentional integer division
		//The average position of all points in the cluster
		Cluster[] clusters = new Cluster[clusterCount];
		//A map from a pixel's location to that pixel's cluster assignment 
		HashMap<Point, Integer> clusterAssignment = new HashMap<Point, Integer>(); 
		//The number of pixels that have been assigned to a different cluster
		//than they were assigned last iteration
		int pixelsMoved = image.getHeight() * image.getWidth();
		//The number of iterations of this algorithm performed
		int iterations = 0;
		Random rnd = new Random();
		
		//Create the clusters
		for (int c = 0; c < clusterCount; ++c)
		{
			clusters[c] = new Cluster();
		}
		
		//Assign each pixel to a random cluster
		for (int x = 0; x < image.getWidth(); ++x)
		{
			for (int y = 0; y < image.getHeight(); ++y)
			{
				int c = rnd.nextInt(clusterCount);
				Color pixelColor = new Color(image.getRGB(x, y));
				Point position = new Point(x, y);
				clusterAssignment.put(position, c);
				clusters[c].addPixel(position, pixelColor);
			}
		}
		
		while (pixelsMoved > MIN_PIXELS_CHANGED && iterations < MAX_ITERATIONS)
		{
			pixelsMoved = 0;
			
			//Reassign each pixel to the best cluster
			for (int x = 0; x < image.getWidth(); ++x)
			{
				for (int y = 0; y < image.getHeight(); ++y)
				{
					int bestCluster = -1;
					double smallestDistance = Double.MAX_VALUE;
					Point position = new Point(x, y);
					for (int c = 0; c < clusterCount; ++c)
					{
						Color pixelColor = new Color(image.getRGB(x, y));
						double distance = clusters[c].distance(position,
								pixelColor, image.getWidth(),
								image.getHeight());
						if (distance < smallestDistance)
						{
							bestCluster = c;
							smallestDistance = distance;
						}
					}
					int oldCluster = clusterAssignment.get(position);
					if (oldCluster != bestCluster)
					{
						pixelsMoved += 1;
					}
					clusterAssignment.put(position, bestCluster);
				}
			}

			//Recalculate the cluster positions and colors
			for (int c = 0; c < clusterCount; ++c)
			{
				clusters[c].clear();
			}
			for (int x = 0; x < image.getWidth(); ++x)
			{
				for (int y = 0; y < image.getHeight(); ++y)
				{
					Point position = new Point(x, y);
					int c = clusterAssignment.get(position);
					Color pixelColor = new Color(image.getRGB(x, y));
					clusters[c].addPixel(position, pixelColor);
				}
			}
			
			if (LOGGING)
			{
				System.out.print("iteration ");
				System.out.print(iterations);
				System.out.print(" : ");
				System.out.print(pixelsMoved);
				System.out.println(" pixels changed clusters");
			}
			
			iterations += 1;
		}
		
		//Draw the an image of the cluster assignments
		if (VISUALIZE_ASSIGNMENTS)
		{
			for (int x = 0; x < image.getWidth(); ++x)
			{
				for (int y = 0; y < image.getHeight(); ++y)
				{
					int c = clusterAssignment.get(new Point(x, y));
					image.setRGB(x, y, clusters[c].getColor().getRGB());
					
				}
			}
			File outfile = new File("assignments.png");
			try {
				ImageIO.write(image, "png", outfile);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		if (VISUALIZE_CLUSTERS)
		{
			BufferedImage clusterImage = new BufferedImage(image.getWidth(),
					image.getHeight(), BufferedImage.TYPE_INT_RGB);
			Graphics g = clusterImage.createGraphics();
			for (int c = 0; c < clusterCount; ++c)
			{
				int radius = (int) Math.sqrt(clusters[c].getArea() / Math.PI);
				g.setColor(clusters[c].getColor());
				g.fillOval((int)clusters[c].getX() - radius,
						(int)clusters[c].getY() - radius, radius * 2,
						radius * 2);
			}
			for (int c = clusterCount - 1; c >= 0; --c)
			{
				int radius = (int) Math.sqrt(clusters[c].getArea() / Math.PI);
				g.setColor(clusters[c].getColor());
				g.drawOval((int)clusters[c].getX() - radius,
						(int)clusters[c].getY() - radius, radius * 2,
						radius * 2);
			}
			File outfile = new File("clusters.png");
			try {
				ImageIO.write(clusterImage, "png", outfile);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		//Generate an array of doubles between 0 and 1 that summarize the image
		ArrayList<Double> result = new ArrayList<Double>();
		//Sort them by their x coordinate
		Arrays.sort(clusters);
		double imageArea = image.getWidth() * image.getHeight();
		for (int c = 0; c < clusterCount; ++c)
		{
			result.add(clusters[c].getX() / image.getWidth());
			result.add(clusters[c].getY() / image.getHeight());
			result.add(clusters[c].getArea() / imageArea);
			result.add(clusters[c].getRed() / 255.0);
			result.add(clusters[c].getGreen() / 255.0);
			result.add(clusters[c].getBlue() / 255.0);
		}
		
		//Pad the output with zeroes to match the requested output length
		while (result.size() < outputLength)
		{
			result.add(0.0);
		}
		
		return result;
	}
	
 
	
	/*
	 * Used by Stephen to manually test this class as he writes it.
	 */
	public static void main(String[] args)
	{
		BufferedImage image = null;
	    URL url;
		try {
			url = new URL("http://upload.wikimedia.org/wikipedia/en/d/d1/Picasso_three_musicians_moma_2006.jpg");
			image = ImageIO.read(url);
			ClusteringMethod processor = new ClusteringMethod();
			ArrayList<Double> result = processor.process(image, 225);

			System.out.println(result);
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}
